package crmdna.list_do_not_use;

import static crmdna.common.OfyService.ofy;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.logging.Logger;

import com.googlecode.objectify.Key;
import com.googlecode.objectify.ObjectifyFilter;
import com.googlecode.objectify.cmd.Query;

import crmdna.common.APIException;
import crmdna.common.APIResponse.Status;
import crmdna.common.Utils;
import crmdna.sequence.Sequence;
import crmdna.sequence.Sequence.SequenceType;

public class ListCore {

	static final String DELIMITER = "||";
	static final int MAX_ELEMENTS_PER_SHARD = 5000;

	// GAE hard limit for multi-valued property is 5000

	final static int MAX_SHARDS = 100;

	// While MAX_SHARDS is not needed as a hard limit, beyond a certain point
	// the implementation would not perform well

	public static ListProp create(String client, long groupId,
			String displayName) {

		Utils.ensureNotNullOrEmpty(displayName, "name cannot be empty");

		String name = displayName.toLowerCase();

		List<Key<ListEntity>> keys = ofy(client).load().type(ListEntity.class)
				.filter("name", name).filter("groupId", groupId).keys().list();

		if (keys.size() != 0)
			throw new APIException(Status.ERROR_RESOURCE_ALREADY_EXISTS,
					"There is already a list [" + displayName + "] for group ["
							+ groupId + "]");

		ListEntity listEntity = new ListEntity();
		listEntity.listId = Sequence.getNext(client, SequenceType.LIST);
		listEntity.displayName = displayName;
		listEntity.groupId = groupId;
		listEntity.name = name;

		ofy(client).save().entity(listEntity).now();

		return listEntity.toProp();
	}

	public static void update(String client, long listId,
			String newDisplayName, Long newGroupId) {
		ListEntity listEntity = safeGet(client, listId);

		if ((newGroupId == null) && (newGroupId == null))
			return;

		if (newGroupId != null)
			listEntity.groupId = newGroupId;

		if (newDisplayName != null) {
			Utils.ensureNotNullOrEmpty(newDisplayName,
					"newDisplayName is empty string");

			listEntity.displayName = newDisplayName;
			listEntity.name = listEntity.displayName.toLowerCase();
		}

		List<Key<ListEntity>> keys = ofy(client).load().type(ListEntity.class)
				.filter("name", listEntity.name)
				.filter("groupId", listEntity.groupId).keys().list();

		if (keys.size() != 0)
			throw new APIException(Status.ERROR_RESOURCE_ALREADY_EXISTS,
					"There is already a list [" + listEntity.displayName
							+ "] for group [" + listEntity.groupId + "]");

		ofy(client).save().entity(listEntity).now();
		return;
	}

	public static void delete(String client, long listId) {
		ListEntity listEntity = safeGet(client, listId);

		List<ListShardEntity> listShardEntities = getShards(client, listId);

		ofy(client).delete().entities(listShardEntities, listEntity).now();
	}

	public static List<ListProp> query(String client, List<Long> groupIds) {

		Query<ListEntity> query = ofy(client).load().type(ListEntity.class);
		if ((groupIds != null) && !groupIds.isEmpty())
			query = query.filter("groupId in", groupIds);

		List<ListEntity> listEntities = query.list();

		List<ListProp> listProps = new ArrayList<>();
		for (ListEntity listEntity : listEntities) {
			listProps.add(listEntity.toProp());
		}

		return listProps;
	}

	public static ListProp addElements(String client, long listId,
			Set<Long> elements) {

		ListEntity listEntity = safeGet(client, listId);

		if (elements.size() == 0)
			return listEntity.toProp();

		// find out memberIds that are not already present
		Set<Long> existingElements = getElements(client, listId);

		elements.removeAll(existingElements);

		if (elements.size() == 0)
			return listEntity.toProp();

		Queue<Long> queue = new LinkedList<>();
		queue.addAll(elements);

		List<ListShardEntity> listShardEntities = getShardsWithAvailableSpace(
				client, listId);

		List<ListShardEntity> shardEntitiesToBeSaved = new ArrayList<>();

		for (ListShardEntity listShardEntity : listShardEntities) {

			shardEntitiesToBeSaved.add(listShardEntity);
			int numSpacesAvailable = listShardEntity.getNumSpacesAvailable();

			for (int i = 0; i < numSpacesAvailable; i++) {
				if (queue.isEmpty())
					break;

				listShardEntity.addElement(queue.remove());
			}

			if (queue.isEmpty())
				break;
		}

		while (!queue.isEmpty()) {
			ListShardEntity listShardEntity = new ListShardEntity();

			if (listEntity.numShards == MAX_SHARDS) {
				Utils.throwIncorrectSpecException("Cannot create any more shards for list ["
						+ listId
						+ "] as the maximum allowed number of shards ["
						+ MAX_SHARDS + "] will be breached");
			}

			listEntity.numShards += 1;
			listShardEntity.listShardId = getComputedShardId(listId,
					listEntity.numShards);
			shardEntitiesToBeSaved.add(listShardEntity);

			int numSpacesAvailable = listShardEntity.getNumSpacesAvailable();

			for (int i = 0; i < numSpacesAvailable; i++) {
				if (queue.isEmpty())
					break;

				listShardEntity.addElement(queue.remove());
			}
		}

		listEntity.numElements += elements.size();
		ofy(client).save().entities(shardEntitiesToBeSaved);
		ofy(client).save().entities(listEntity);

		ObjectifyFilter.complete();

		return listEntity.toProp();
	}

	public static void addElementsFromAnotherList(String client, long listId,
			long anotherListId) {

		safeGet(client, listId);
		safeGet(client, anotherListId);

		Set<Long> elements = getElements(client, listId);

		addElements(client, listId, elements);
	}

	static void deleteElements(String client, long listId, Set<Long> elements) {

		ListEntity listEntity = ListCore.safeGet(client, listId);

		Set<Long> existingMemberIds = getElements(client, listId);

		elements.retainAll(existingMemberIds);

		List<ListShardEntity> listShardEntities = getShards(client, listId);
		List<ListShardEntity> shardEntitiesToBeSaved = new ArrayList<>();

		for (ListShardEntity listShardEntity : listShardEntities) {
			boolean changed = listShardEntity.deleteElements(elements);

			if (changed)
				shardEntitiesToBeSaved.add(listShardEntity);
		}

		listEntity.numElements -= elements.size();

		ofy(client).save().entities(shardEntitiesToBeSaved, listEntity).now();
	}

	static Set<Long> getElements(String client, long listId) {
		safeGet(client, listId);

		Set<Long> elements = new HashSet<>();

		List<ListShardEntity> shardEntities = getShards(client, listId);
		for (ListShardEntity listShardEntity : shardEntities) {
			elements.addAll(listShardEntity.getElements());
		}

		return elements;
	}

	public static List<ListProp> getListsContainingElement(String client,
			long element) {

		List<Key<ListShardEntity>> keys = ofy(client).load()
				.type(ListShardEntity.class).filter("elements", element).keys()
				.list();

		if (keys.size() == 0)
			return new ArrayList<>();

		Set<Long> listIds = new HashSet<>();

		for (Key<ListShardEntity> key : keys) {
			String listShardId = key.getName();

			try {
				ListShardProp listShardProp = new ListShardProp(listShardId);
				listIds.add(listShardProp.listId);
			} catch (Exception ex) {
				// throws exception if listShardId is in wrong format
				// log the exception but don't let it propagate
				Logger logger = Logger.getLogger(ListCore.class.getName());
				logger.severe("listShardId [" + listShardId
						+ "] computed from listShardEntity key ["
						+ key.toString() + "] is in the wrong format");
			}
		}

		Map<Long, ListEntity> map = ofy(client).load().type(ListEntity.class)
				.ids(listIds);

		List<ListProp> listProps = new ArrayList<>();
		for (Long listId : map.keySet()) {
			listProps.add(map.get(listId).toProp());
		}

		return listProps;
	}

	private static ListEntity safeGet(String client, long listId) {
		ListEntity listEntity = ofy(client).load().type(ListEntity.class)
				.id(listId).get();

		if (null == listEntity)
			throw new APIException(Status.ERROR_RESOURCE_NOT_FOUND,
					"There is no list with id [" + listId + "]");

		return listEntity;
	}

	static List<ListShardEntity> getShards(String client, long listId) {

		ListEntity listEntity = safeGet(client, listId);

		List<String> shardIds = getComputedShardIds(listId,
				listEntity.numShards);

		Map<String, ListShardEntity> map = ofy(client).load()
				.type(ListShardEntity.class).ids(shardIds);

		List<ListShardEntity> shardEntities = new ArrayList<>();
		for (String s : map.keySet()) {
			shardEntities.add(map.get(s));
		}

		return shardEntities;
	}

	static List<ListShardEntity> getShardsWithAvailableSpace(String client,
			long listId) {

		safeGet(client, listId);

		List<ListShardEntity> all = getShards(client, listId);

		List<ListShardEntity> spaceAvailable = new ArrayList<>();
		for (ListShardEntity listShardEntity : all) {
			if (listShardEntity.isSpaceAvailable())
				spaceAvailable.add(listShardEntity);
		}

		return spaceAvailable;
	}

	static List<String> getComputedShardIds(long listId, int numShards) {
		// if listId is 1
		// possible shardIds are: 1||1, 1||2, 1||3, and so on

		List<String> shardIds = new ArrayList<>();

		for (int i = 0; i < numShards; i++) {
			int shardNo = i + 1;
			String shardId = getComputedShardId(listId, shardNo);
			shardIds.add(shardId);
		}

		return shardIds;
	}

	static String getComputedShardId(long listId, int shardNo) {
		return listId + DELIMITER + shardNo;
	}
}
